/**
 * Copyright (C) 2009-2012, KingHool.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses.
 */
package com.kinghool.smodel.io.json;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import com.kinghool.ebos.commons.exception.ErrorCodeRuntimeException;
import com.kinghool.ebos.commons.utils.Assert;
import com.kinghool.ebos.commons.utils.NameHelper;
import com.kinghool.ebos.commons.utils.Utils;
import com.kinghool.smodel.ErrorCodes;
import com.kinghool.smodel.IModel;
import com.kinghool.smodel.ITreeModel;
import com.kinghool.smodel.IXmlModel;
import com.kinghool.smodel.TreeModel;
import com.kinghool.smodel.XmlModel;
import com.kinghool.smodel.io.IModelChannel;

/**
 * 
 * @Author Allen Deng(dengtailin@gmail.com) 
 */
public class JsonStreamChannel implements IModelChannel {
	private final Reader reader;
	private final Writer writer;
	
	private String rootId;
	
	private final Set<String> childrenList = Utils.newSet();

	protected final static String ATTR = "$attr";
	protected final static String VALUE = "$value"; 
	
	protected final static List<String> DEFAULT_CHILDREN = Arrays.asList(new String[]{ATTR, VALUE});
	
	public JsonStreamChannel(Reader reader, Writer writer) {
		this.reader = reader;
		this.writer = writer;
	}
	
	public JsonStreamChannel(InputStream inputStream, OutputStream outputStream) {
		this(new InputStreamReader(inputStream), new OutputStreamWriter(outputStream));
	}
   
	public JsonStreamChannel(String inputFileName, String outputFileName) throws FileNotFoundException {
		this(new InputStreamReader(new FileInputStream(inputFileName)), 
			 new OutputStreamWriter(new FileOutputStream(outputFileName)));
	}
	
	/**
	 * for sub class only
	 */
	protected JsonStreamChannel() {
		this.reader = null;
		this.writer = null;
	}
	
	protected Reader getReader() {
		return reader;
	}
	
	protected Writer getWriter() {
		return writer;
	}
	
	protected void afterLoad(Writer writer) {
	}
	
	@SuppressWarnings("unchecked")
	public void flushModel(IModel model) {
		JSONObject jsonObject = new JSONObject();
		JSONObject jsonModelObject = new JSONObject();
		
		flushModel(model, jsonModelObject);
		
		jsonObject.put(model.getModelId(), jsonModelObject);
		
		Writer writer = getWriter();
		try {
			jsonObject.writeJSONString(writer);
			writer.flush();
			
			afterLoad(writer);
		} catch (IOException e) {
			throw new ErrorCodeRuntimeException(ErrorCodes.SMODEL_003.cloneWithArgs("json"), e);
		}
	}
 
	public void loadModel(IModel model) {
		model.reset();
		
		Object object = JSONValue.parse(getReader());
		
		//the parsed string should be json object
        if (object instanceof JSONObject) {
        	JSONObject jsonObject = (JSONObject) object;

    		//the parsed json object should have a root object
        	if (jsonObject.keySet().size() == 1) {
        		Object rootName = jsonObject.keySet().iterator().next();
        		Object rootObject = jsonObject.get(rootName);
        		if (rootObject instanceof JSONObject) {
        			model.setModelId(String.valueOf(rootName));
        			loadModel(model, (JSONObject) rootObject);
        		}
        	}
        }
	}
	
	public void loadModel(IModel model, JSONObject jsonObject) {
		// flush attribute
		loadAttribute(model, jsonObject);

		// flush text
		if (model instanceof IXmlModel) {
			Object object = jsonObject.get(VALUE);
			if (object != null) {
			    ((IXmlModel) model).setText(String.valueOf(object));
			}
		}
		
		// flush children
		if (model instanceof ITreeModel) {
			loadChildren((ITreeModel) model, jsonObject);
		}
	}
	
	protected ITreeModel newChildModel(ITreeModel parent, String tag) {
		if (parent instanceof IXmlModel) {
			return new XmlModel(tag, (IXmlModel)parent);
		} else {
			return new TreeModel(tag, parent);
		}
	}
	
	protected void loadChildren(ITreeModel model, JSONObject jsonObject) {
		for (Object childName : jsonObject.keySet()) {
    		if (!DEFAULT_CHILDREN.contains(childName) && childName != null) {
    			Object jsonChild = jsonObject.get(childName);
    			if (jsonChild != null) {
	    			if (jsonChild instanceof JSONArray) {
	    				for (Object aChild : (JSONArray)jsonChild) {
	    					//all the child in the list should be json object
	    					if (aChild instanceof JSONObject) {
	    						ITreeModel child = newChildModel(model, String.valueOf(childName));
	    						loadModel(child, (JSONObject) aChild);
	    						model.addChild(child);
	    					}
	    				}
	    			} else if (jsonChild instanceof JSONObject){
						ITreeModel child = newChildModel(model, String.valueOf(childName));
						loadModel(child, (JSONObject) jsonChild);
						model.addChild(child);
	    			} else {
	    				if (model instanceof IXmlModel) {
    						((IXmlModel)model).setChildText(String.valueOf(childName), String.valueOf(jsonChild));
	    				}
	    			}
    			}
    		}
    	}
	}
	
	protected void loadAttribute(IModel model, JSONObject jsonObject) {
		Object object = jsonObject.get(ATTR);
		
		//the parsed ATTR should be json object
		if (object instanceof JSONObject) {
			JSONObject jsonAttrObject = (JSONObject) object;
			for (Object attr : jsonAttrObject.keySet()) {
				if (attr != null) {
					model.setProperty(String.valueOf(attr), jsonAttrObject.get(attr));
				}
			}
        }
	}
	
	public Object getMappedObject() {
		throw new UnsupportedOperationException("use JsonChannel instead.");
	} 
	 
	public void setMappedObject(Object xml) {
		throw new UnsupportedOperationException("use JsonChannel instead.");
	}

	public String getRootId() {
		return rootId;
	}

	public void setRootId(String rootId) {
		this.rootId = rootId;
	}

	public boolean isArrayChild(String childFullName) {
		return childrenList.contains(NameHelper.formatName(childFullName));
	}

	public void addChildrenList(String childFullName) {
		this.childrenList.add(NameHelper.formatName(childFullName));
	}
	
	@SuppressWarnings("unchecked")
	protected void flushModel(IModel model, JSONObject jsonModelObject) {
		// load attribute
		if (!Assert.isNullOrEmpty(model.getAllProperties())) {
			JSONObject attrObject = new JSONObject();
			jsonModelObject.put(ATTR, attrObject);
			loadAttr(model, attrObject);
		}

		// load text if its is IXmlModel
		if (model instanceof IXmlModel) {
			if (Assert.isNotNullNotEmpty(((IXmlModel) model).getText())) {
				jsonModelObject.put(VALUE, ((IXmlModel) model).getText());
			}
		}

		// load child if it is a ITreeModel
		if (model instanceof ITreeModel) {
			flushChildren((ITreeModel) model, jsonModelObject);
		}
	}
	
	@SuppressWarnings("unchecked")
	protected void flushChildren(ITreeModel model, JSONObject jsonModelObject) {
		for (String childName : model.getChildNames()) {
			List<ITreeModel> children = model.getTreeChild(childName);
			if (!Assert.isNullOrEmpty(children)) {				
				if (children.size() == 1 && !isArrayChild(NameHelper.buildFullName(model.toString(), childName))) {
					ITreeModel child = children.get(0);
					if (Assert.isNullOrEmpty(child.getAllProperties()) 
					    && !child.hasChildren() 
					    && child instanceof IXmlModel
					    && Assert.isNotNullNotEmpty(((IXmlModel)child).getText())) {
						jsonModelObject.put(childName, ((IXmlModel)child).getText());
					} else {
						JSONObject jsonChild = new JSONObject();
						flushModel(child, jsonChild);
						jsonModelObject.put(childName, jsonChild);
					}
				} else {
					JSONArray childArray = new JSONArray();
					for (ITreeModel child : children) {
						JSONObject jsonChild = new JSONObject();
						flushModel(child, jsonChild);
						childArray.add(jsonChild);
					}
					jsonModelObject.put(childName, childArray);
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	protected void loadAttr(IModel model, JSONObject attrObject) {
		List<String> props = Utils.newList();
		if (!model.getModelDefinition().isIgnoreCheck()) {
			props = model.getModelDefinition().getPropertySequence();
			for (Iterator<String> iter = props.iterator(); iter.hasNext();) {
				if (model.isUnset(iter.next())) {
					iter.remove();
				}
			}
		} else {
			props.addAll(new ArrayList<String>(model.getAllProperties()));
		}
		
		//set attribute for element	
		for (String propName : props) {
			String propVal = model.getStringProperty(propName);
			if (Assert.isNotNullNotEmpty(propVal)) {
				attrObject.put(propName, propVal);
			}
		}
	} 
}
